/*
 * angucomplete-alt
 * Autocomplete directive for AngularJS
 * This is a fork of Daryl Rowland's angucomplete with some extra features.
 * By Hidenari Nozaki
 */

/*! Copyright (c) 2014 Hidenari Nozaki and contributors | Licensed under the MIT license */

(function (root, factory) {
    'use strict';
    if (typeof module !== 'undefined' && module.exports) {
        // CommonJS
        module.exports = factory(require('angular'));
    } else if (typeof define === 'function' && define.amd) {
        // AMD
        define(['angular'], factory);
    } else {
        // Global Variables
        factory(root.angular);
    }
}(window, function (angular) {
    'use strict';

    angular.module('angucomplete-alt', []).directive('angucompleteAlt', ['$q', '$parse', '$http', '$sce', '$timeout', '$templateCache', '$interpolate', function ($q, $parse, $http, $sce, $timeout, $templateCache, $interpolate) {
        // keyboard events
        var KEY_DW = 40;
        var KEY_RT = 39;
        var KEY_UP = 38;
        var KEY_LF = 37;
        var KEY_ES = 27;
        var KEY_EN = 13;
        var KEY_TAB = 9;

        var MIN_LENGTH = 3;
        var MAX_LENGTH = 524288;  // the default max length per the html maxlength attribute
        var PAUSE = 500;
        var BLUR_TIMEOUT = 200;

        // string constants
        var REQUIRED_CLASS = 'autocomplete-required';
        var TEXT_SEARCHING = 'Searching...';
        var TEXT_NORESULTS = 'No results found';
        var TEMPLATE_URL = '/angucomplete-alt/index.html';

        // Set the default template for this directive
        $templateCache.put(TEMPLATE_URL,
            '<div class="angucomplete-holder" ng-class="{\'angucomplete-dropdown-visible\': showDropdown}">' +
            '  <input id="{{id}}_value" name="{{inputName}}" tabindex="{{fieldTabindex}}" ng-class="{\'angucomplete-input-not-empty\': notEmpty}" ng-model="searchStr" ng-disabled="disableInput" type="{{inputType}}" placeholder="{{placeholder}}" maxlength="{{maxlength}}" ng-focus="onFocusHandler()" class="{{inputClass}}" ng-focus="resetHideResults()" ng-blur="hideResults($event)" autocapitalize="off" autocorrect="off" autocomplete="off" ng-change="inputChangeHandler(searchStr)"/>' +
            '  <div id="{{id}}_dropdown" class="angucomplete-dropdown" ng-show="showDropdown">' +
            '    <div class="angucomplete-searching" ng-show="searching" ng-bind="textSearching"></div>' +
            '    <div class="angucomplete-searching" ng-show="!searching && (!results || results.length == 0)" ng-bind="textNoResults"></div>' +
            '    <div class="angucomplete-row" ng-repeat="result in results" ng-click="selectResult(result)" ng-mouseenter="hoverRow($index)" ng-class="{\'angucomplete-selected-row\': $index == currentIndex}">' +
            '      <div ng-if="imageField" class="angucomplete-image-holder">' +
            '        <img ng-if="result.image && result.image != \'\'" ng-src="{{result.image}}" class="angucomplete-image"/>' +
            '        <div ng-if="!result.image && result.image != \'\'" class="angucomplete-image-default"></div>' +
            '      </div>' +
            '      <div class="angucomplete-title" ng-if="matchClass" ng-bind-html="result.title"></div>' +
            '      <div class="angucomplete-title" ng-if="!matchClass">{{ result.title }}</div>' +
            '      <div ng-if="matchClass && result.description && result.description != \'\'" class="angucomplete-description" ng-bind-html="result.description"></div>' +
            '      <div ng-if="!matchClass && result.description && result.description != \'\'" class="angucomplete-description">{{result.description}}</div>' +
            '    </div>' +
            '  </div>' +
            '</div>'
        );

        function link(scope, elem, attrs, ctrl) {
            var inputField = elem.find('input');
            var minlength = MIN_LENGTH;
            var searchTimer = null;
            var hideTimer;
            var requiredClassName = REQUIRED_CLASS;
            var responseFormatter;
            var validState = null;
            var httpCanceller = null;
            var httpCallInProgress = false;
            var dd = elem[0].querySelector('.angucomplete-dropdown');
            var isScrollOn = false;
            var mousedownOn = null;
            var unbindInitialValue;
            var displaySearching;
            var displayNoResults;

            elem.on('mousedown', function (event) {
                if (event.target.id) {
                    mousedownOn = event.target.id;
                    if (mousedownOn === scope.id + '_dropdown') {
                        document.body.addEventListener('click', clickoutHandlerForDropdown);
                    }
                }
                else {
                    mousedownOn = event.target.className;
                }
            });

            scope.currentIndex = scope.focusFirst ? 0 : null;
            scope.searching = false;
            unbindInitialValue = scope.$watch('initialValue', function (newval) {
                if (newval) {
                    // remove scope listener
                    unbindInitialValue();
                    // change input
                    handleInputChange(newval, true);
                }
            });

            scope.$watch('fieldRequired', function (newval, oldval) {
                if (newval !== oldval) {
                    if (!newval) {
                        ctrl[scope.inputName].$setValidity(requiredClassName, true);
                    }
                    else if (!validState || scope.currentIndex === -1) {
                        handleRequired(false);
                    }
                    else {
                        handleRequired(true);
                    }
                }
            });

            scope.$on('angucomplete-alt:clearInput', function (event, elementId) {
                if (!elementId || elementId === scope.id) {
                    scope.searchStr = null;
                    callOrAssign();
                    handleRequired(false);
                    clearResults();
                }
            });

            scope.$on('angucomplete-alt:changeInput', function (event, elementId, newval) {
                if (!!elementId && elementId === scope.id) {
                    handleInputChange(newval);
                }
            });

            function handleInputChange(newval, initial) {
                if (newval) {
                    if (typeof newval === 'object') {
                        scope.searchStr = extractTitle(newval);
                        callOrAssign({originalObject: newval});
                    } else if (typeof newval === 'string' && newval.length > 0) {
                        scope.searchStr = newval;
                    } else {
                        if (console && console.error) {
                            console.error('Tried to set ' + (!!initial ? 'initial' : '') + ' value of angucomplete to', newval, 'which is an invalid value');
                        }
                    }

                    handleRequired(true);
                }
            }

            // #194 dropdown list not consistent in collapsing (bug).
            function clickoutHandlerForDropdown(event) {
                mousedownOn = null;
                scope.hideResults(event);
                document.body.removeEventListener('click', clickoutHandlerForDropdown);
            }

            // for IE8 quirkiness about event.which
            function ie8EventNormalizer(event) {
                return event.which ? event.which : event.keyCode;
            }

            function callOrAssign(value) {
                if (typeof scope.selectedObject === 'function') {
                    scope.selectedObject(value, scope.selectedObjectData);
                }
                else {
                    scope.selectedObject = value;
                }

                if (value) {
                    handleRequired(true);
                }
                else {
                    handleRequired(false);
                }
            }

            function callFunctionOrIdentity(fn) {
                return function (data) {
                    return scope[fn] ? scope[fn](data) : data;
                };
            }

            function setInputString(str) {
                callOrAssign({originalObject: str});

                if (scope.clearSelected) {
                    scope.searchStr = null;
                }
                clearResults();
            }

            function extractTitle(data) {
                // split title fields and run extractValue for each and join with ' '
                return scope.titleField.split(',')
                    .map(function (field) {
                        return extractValue(data, field);
                    })
                    .join(' ');
            }

            function extractValue(obj, key) {
                var keys, result;
                if (key) {
                    keys = key.split('.');
                    result = obj;
                    for (var i = 0; i < keys.length; i++) {
                        result = result[keys[i]];
                    }
                }
                else {
                    result = obj;
                }
                return result;
            }

            function findMatchString(target, str) {
                var result, matches, re;
                // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
                // Escape user input to be treated as a literal string within a regular expression
                re = new RegExp(str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
                if (!target) {
                    return;
                }
                if (!target.match || !target.replace) {
                    target = target.toString();
                }
                matches = target.match(re);
                if (matches) {
                    result = target.replace(re,
                        '<span class="' + scope.matchClass + '">' + matches[0] + '</span>');
                }
                else {
                    result = target;
                }
                return $sce.trustAsHtml(result);
            }

            function handleRequired(valid) {
                scope.notEmpty = valid;
                validState = scope.searchStr;
                if (scope.fieldRequired && ctrl && scope.inputName) {
                    ctrl[scope.inputName].$setValidity(requiredClassName, valid);
                }
            }

            function keyupHandler(event) {
                var which = ie8EventNormalizer(event);
                if (which === KEY_LF || which === KEY_RT) {
                    // do nothing
                    return;
                }

                if (which === KEY_UP || which === KEY_EN) {
                    event.preventDefault();
                }
                else if (which === KEY_DW) {
                    event.preventDefault();
                    if (!scope.showDropdown && scope.searchStr && scope.searchStr.length >= minlength) {
                        initResults();
                        scope.searching = true;
                        searchTimerComplete(scope.searchStr);
                    }
                }
                else if (which === KEY_ES) {
                    clearResults();
                    scope.$apply(function () {
                        inputField.val(scope.searchStr);
                    });
                }
                else {
                    if (minlength === 0 && !scope.searchStr) {
                        return;
                    }

                    if (!scope.searchStr || scope.searchStr === '') {
                        scope.showDropdown = false;
                    } else if (scope.searchStr.length >= minlength) {
                        initResults();

                        if (searchTimer) {
                            $timeout.cancel(searchTimer);
                        }

                        scope.searching = true;

                        searchTimer = $timeout(function () {
                            searchTimerComplete(scope.searchStr);
                        }, scope.pause);
                    }

                    if (validState && validState !== scope.searchStr && !scope.clearSelected) {
                        scope.$apply(function () {
                            callOrAssign();
                        });
                    }
                }
            }

            function handleOverrideSuggestions(event) {
                if (scope.overrideSuggestions &&
                    !(scope.selectedObject && scope.selectedObject.originalObject === scope.searchStr)) {
                    if (event) {
                        event.preventDefault();
                    }

                    // cancel search timer
                    $timeout.cancel(searchTimer);
                    // cancel http request
                    cancelHttpRequest();

                    setInputString(scope.searchStr);
                }
            }

            function dropdownRowOffsetHeight(row) {
                var css = getComputedStyle(row);
                return row.offsetHeight +
                    parseInt(css.marginTop, 10) + parseInt(css.marginBottom, 10);
            }

            function dropdownHeight() {
                return dd.getBoundingClientRect().top +
                    parseInt(getComputedStyle(dd).maxHeight, 10);
            }

            function dropdownRow() {
                return elem[0].querySelectorAll('.angucomplete-row')[scope.currentIndex];
            }

            function dropdownRowTop() {
                return dropdownRow().getBoundingClientRect().top -
                    (dd.getBoundingClientRect().top +
                        parseInt(getComputedStyle(dd).paddingTop, 10));
            }

            function dropdownScrollTopTo(offset) {
                dd.scrollTop = dd.scrollTop + offset;
            }

            function updateInputField() {
                var current = scope.results[scope.currentIndex];
                if (scope.matchClass) {
                    inputField.val(extractTitle(current.originalObject));
                }
                else {
                    inputField.val(current.title);
                }
            }

            function keydownHandler(event) {
                var which = ie8EventNormalizer(event);
                var row = null;
                var rowTop = null;

                if (which === KEY_EN && scope.results) {
                    if (scope.currentIndex >= 0 && scope.currentIndex < scope.results.length) {
                        event.preventDefault();
                        scope.selectResult(scope.results[scope.currentIndex]);
                    } else {
                        handleOverrideSuggestions(event);
                        clearResults();
                    }
                    scope.$apply();
                } else if (which === KEY_DW && scope.results) {
                    event.preventDefault();
                    if ((scope.currentIndex + 1) < scope.results.length && scope.showDropdown) {
                        scope.$apply(function () {
                            scope.currentIndex++;
                            updateInputField();
                        });

                        if (isScrollOn) {
                            row = dropdownRow();
                            if (dropdownHeight() < row.getBoundingClientRect().bottom) {
                                dropdownScrollTopTo(dropdownRowOffsetHeight(row));
                            }
                        }
                    }
                } else if (which === KEY_UP && scope.results) {
                    event.preventDefault();
                    if (scope.currentIndex >= 1) {
                        scope.$apply(function () {
                            scope.currentIndex--;
                            updateInputField();
                        });

                        if (isScrollOn) {
                            rowTop = dropdownRowTop();
                            if (rowTop < 0) {
                                dropdownScrollTopTo(rowTop - 1);
                            }
                        }
                    }
                    else if (scope.currentIndex === 0) {
                        scope.$apply(function () {
                            scope.currentIndex = -1;
                            inputField.val(scope.searchStr);
                        });
                    }
                } else if (which === KEY_TAB) {
                    if (scope.results && scope.results.length > 0 && scope.showDropdown) {
                        if (scope.currentIndex === -1 && scope.overrideSuggestions) {
                            // intentionally not sending event so that it does not
                            // prevent default tab behavior
                            handleOverrideSuggestions();
                        }
                        else {
                            if (scope.currentIndex === -1) {
                                scope.currentIndex = 0;
                            }
                            scope.selectResult(scope.results[scope.currentIndex]);
                            scope.$digest();
                        }
                    }
                    else {
                        // no results
                        // intentionally not sending event so that it does not
                        // prevent default tab behavior
                        if (scope.searchStr && scope.searchStr.length > 0) {
                            handleOverrideSuggestions();
                        }
                    }
                } else if (which === KEY_ES) {
                    // This is very specific to IE10/11 #272
                    // without this, IE clears the input text
                    event.preventDefault();
                }
            }

            function httpSuccessCallbackGen(str) {
                return function (responseData, status, headers, config) {
                    // normalize return obejct from promise
                    if (!status && !headers && !config && responseData.data) {
                        responseData = responseData.data;
                    }
                    scope.searching = false;
                    processResults(
                        extractValue(responseFormatter(responseData), scope.remoteUrlDataField),
                        str);
                };
            }

            function httpErrorCallback(errorRes, status, headers, config) {
                scope.searching = httpCallInProgress;

                // normalize return obejct from promise
                if (!status && !headers && !config) {
                    status = errorRes.status;
                }

                // cancelled/aborted
                if (status === 0 || status === -1) {
                    return;
                }
                if (scope.remoteUrlErrorCallback) {
                    scope.remoteUrlErrorCallback(errorRes, status, headers, config);
                }
                else {
                    if (console && console.error) {
                        console.error('http error');
                    }
                }
            }

            function cancelHttpRequest() {
                if (httpCanceller) {
                    httpCanceller.resolve();
                }
            }

            function getRemoteResults(str) {
                var params = {},
                    url = scope.remoteUrl + encodeURIComponent(str);
                if (scope.remoteUrlRequestFormatter) {
                    params = {params: scope.remoteUrlRequestFormatter(str)};
                    url = scope.remoteUrl;
                }
                if (!!scope.remoteUrlRequestWithCredentials) {
                    params.withCredentials = true;
                }
                cancelHttpRequest();
                httpCanceller = $q.defer();
                params.timeout = httpCanceller.promise;
                httpCallInProgress = true;
                $http.get(url, params)
                    .then(httpSuccessCallbackGen(str))
                    .catch(httpErrorCallback)
                    .finally(function () {
                        httpCallInProgress = false;
                    });
            }

            function getRemoteResultsWithCustomHandler(str) {
                cancelHttpRequest();

                httpCanceller = $q.defer();

                scope.remoteApiHandler(str, httpCanceller.promise)
                    .then(httpSuccessCallbackGen(str))
                    .catch(httpErrorCallback);

                /* IE8 compatible
                scope.remoteApiHandler(str, httpCanceller.promise)
                  ['then'](httpSuccessCallbackGen(str))
                  ['catch'](httpErrorCallback);
                */
            }

            function clearResults() {
                scope.showDropdown = false;
                scope.results = [];
                if (dd) {
                    dd.scrollTop = 0;
                }
            }

            function initResults() {
                scope.showDropdown = displaySearching;
                scope.currentIndex = scope.focusFirst ? 0 : -1;
                scope.results = [];
            }

            function getLocalResults(str) {
                var i, match, s, value,
                    searchFields = scope.searchFields.split(','),
                    matches = [];
                if (typeof scope.parseInput() !== 'undefined') {
                    str = scope.parseInput()(str);
                }
                for (i = 0; i < scope.localData.length; i++) {
                    match = false;

                    for (s = 0; s < searchFields.length; s++) {
                        value = extractValue(scope.localData[i], searchFields[s]) || '';
                        match = match || (value.toString().toLowerCase().indexOf(str.toString().toLowerCase()) >= 0);
                    }

                    if (match) {
                        matches[matches.length] = scope.localData[i];
                    }
                }
                return matches;
            }

            function checkExactMatch(result, obj, str) {
                if (!str) {
                    return false;
                }
                for (var key in obj) {
                    if (obj[key].toLowerCase() === str.toLowerCase()) {
                        scope.selectResult(result);
                        return true;
                    }
                }
                return false;
            }

            function searchTimerComplete(str) {
                // Begin the search
                if (!str || str.length < minlength) {
                    return;
                }
                if (scope.localData) {
                    scope.$apply(function () {
                        var matches;
                        if (typeof scope.localSearch() !== 'undefined') {
                            matches = scope.localSearch()(str, scope.localData);
                        } else {
                            matches = getLocalResults(str);
                        }
                        scope.searching = false;
                        processResults(matches, str);
                    });
                }
                else if (scope.remoteApiHandler) {
                    getRemoteResultsWithCustomHandler(str);
                } else {
                    getRemoteResults(str);
                }
            }

            function processResults(responseData, str) {
                var i, description, image, text, formattedText, formattedDesc;

                if (responseData && responseData.length > 0) {
                    scope.results = [];

                    for (i = 0; i < responseData.length; i++) {
                        if (scope.titleField && scope.titleField !== '') {
                            text = formattedText = extractTitle(responseData[i]);
                        }

                        description = '';
                        if (scope.descriptionField) {
                            description = formattedDesc = extractValue(responseData[i], scope.descriptionField);
                        }

                        image = '';
                        if (scope.imageField) {
                            image = extractValue(responseData[i], scope.imageField);
                        }

                        if (scope.matchClass) {
                            formattedText = findMatchString(text, str);
                            formattedDesc = findMatchString(description, str);
                        }

                        scope.results[scope.results.length] = {
                            title: formattedText,
                            description: formattedDesc,
                            image: image,
                            originalObject: responseData[i]
                        };
                    }

                } else {
                    scope.results = [];
                }

                if (scope.autoMatch && scope.results.length === 1 &&
                    checkExactMatch(scope.results[0],
                        {title: text, desc: description || ''}, scope.searchStr)) {
                    scope.showDropdown = false;
                } else if (scope.results.length === 0 && !displayNoResults) {
                    scope.showDropdown = false;
                } else {
                    scope.showDropdown = true;
                }
            }

            function showAll() {
                if (scope.localData) {
                    scope.searching = false;
                    processResults(scope.localData, '');
                }
                else if (scope.remoteApiHandler) {
                    scope.searching = true;
                    getRemoteResultsWithCustomHandler('');
                }
                else {
                    scope.searching = true;
                    getRemoteResults('');
                }
            }

            scope.onFocusHandler = function () {
                minlength = scope.minlength;
                if (scope.focusIn) {
                    scope.focusIn();
                }
                if (minlength === 0 && (!scope.searchStr || scope.searchStr.length === 0)) {
                    scope.currentIndex = scope.focusFirst ? 0 : scope.currentIndex;
                    scope.showDropdown = true;
                    showAll();
                }
            };

            scope.hideResults = function () {
                if (mousedownOn &&
                    (mousedownOn === scope.id + '_dropdown' ||
                        mousedownOn.indexOf('angucomplete') >= 0)) {
                    mousedownOn = null;
                }
                else {
                    hideTimer = $timeout(function () {
                        clearResults();
                        scope.$apply(function () {
                            if (scope.searchStr && scope.searchStr.length > 0) {
                                inputField.val(scope.searchStr);
                            }
                        });
                    }, BLUR_TIMEOUT);
                    cancelHttpRequest();

                    if (scope.searchStr === '' || scope.searchStr === undefined) {
                        callOrAssign();
                    }

                    if (scope.focusOut) {
                        scope.focusOut();
                    }

                    if (scope.overrideSuggestions) {
                        if (scope.searchStr && scope.searchStr.length > 0 && scope.currentIndex === -1) {
                            handleOverrideSuggestions();
                        }
                    }
                }
            };

            scope.resetHideResults = function () {
                if (hideTimer) {
                    $timeout.cancel(hideTimer);
                }
            };

            scope.hoverRow = function (index) {
                scope.currentIndex = index;
            };

            scope.selectResult = function (result) {
                // Restore original values
                if (scope.matchClass) {
                    result.title = extractTitle(result.originalObject);
                    result.description = extractValue(result.originalObject, scope.descriptionField);
                }

                if (scope.clearSelected) {
                    scope.searchStr = null;
                }
                else {
                    scope.searchStr = result.title;
                }
                callOrAssign(result);
                clearResults();
            };

            scope.inputChangeHandler = function (str) {
                if (str.length < minlength) {
                    cancelHttpRequest();
                    clearResults();
                }
                else if (str.length === 0 && minlength === 0) {
                    showAll();
                }

                if (scope.inputChanged) {
                    str = scope.inputChanged(str);
                }
                return str;
            };

            // check required
            if (scope.fieldRequiredClass && scope.fieldRequiredClass !== '') {
                requiredClassName = scope.fieldRequiredClass;
            }

            // check min length
            if (scope.minlength && scope.minlength !== '') {
                minlength = parseInt(scope.minlength, 10);
            }

            // check pause time
            if (!scope.pause) {
                scope.pause = PAUSE;
            }

            // check clearSelected
            if (!scope.clearSelected) {
                scope.clearSelected = false;
            }

            // check override suggestions
            if (!scope.overrideSuggestions) {
                scope.overrideSuggestions = false;
            }

            // check required field
            if (scope.fieldRequired && ctrl) {
                // check initial value, if given, set validitity to true
                if (scope.initialValue) {
                    handleRequired(true);
                }
                else {
                    handleRequired(false);
                }
            }

            scope.inputType = attrs.type ? attrs.type : 'text';

            // set strings for "Searching..." and "No results"
            scope.textSearching = attrs.textSearching ? attrs.textSearching : TEXT_SEARCHING;
            scope.textNoResults = attrs.textNoResults ? attrs.textNoResults : TEXT_NORESULTS;
            displaySearching = scope.textSearching === 'false' ? false : true;
            displayNoResults = scope.textNoResults === 'false' ? false : true;

            // set max length (default to maxlength deault from html
            scope.maxlength = attrs.maxlength ? attrs.maxlength : MAX_LENGTH;

            // register events
            inputField.on('keydown', keydownHandler);
            inputField.on('keyup compositionend', keyupHandler);

            // set response formatter
            responseFormatter = callFunctionOrIdentity('remoteUrlResponseFormatter');

            // set isScrollOn
            $timeout(function () {
                var css = getComputedStyle(dd);
                isScrollOn = css.maxHeight && css.overflowY === 'auto';
            });
        }

        return {
            restrict: 'EA',
            require: '^?form',
            scope: {
                selectedObject: '=',
                selectedObjectData: '=',
                disableInput: '=',
                initialValue: '=',
                localData: '=',
                localSearch: '&',
                remoteUrlRequestFormatter: '=',
                remoteUrlRequestWithCredentials: '@',
                remoteUrlResponseFormatter: '=',
                remoteUrlErrorCallback: '=',
                remoteApiHandler: '=',
                id: '@',
                type: '@',
                placeholder: '@',
                textSearching: '@',
                textNoResults: '@',
                remoteUrl: '@',
                remoteUrlDataField: '@',
                titleField: '@',
                descriptionField: '@',
                imageField: '@',
                inputClass: '@',
                pause: '@',
                searchFields: '@',
                minlength: '=',
                matchClass: '@',
                clearSelected: '@',
                overrideSuggestions: '@',
                fieldRequired: '=',
                fieldRequiredClass: '@',
                inputChanged: '=',
                autoMatch: '@',
                focusOut: '&',
                focusIn: '&',
                fieldTabindex: '@',
                inputName: '@',
                focusFirst: '@',
                parseInput: '&'
            },
            templateUrl: function (element, attrs) {
                return attrs.templateUrl || TEMPLATE_URL;
            },
            compile: function (tElement) {
                var startSym = $interpolate.startSymbol();
                var endSym = $interpolate.endSymbol();
                if (!(startSym === '{{' && endSym === '}}')) {
                    var interpolatedHtml = tElement.html()
                        .replace(/\{\{/g, startSym)
                        .replace(/\}\}/g, endSym);
                    tElement.html(interpolatedHtml);
                }
                return link;
            }
        };
    }]);

}));
